/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.replication;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.transaction.UserTransaction;

import junit.framework.TestCase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.experimental.categories.Category;
import org.springframework.context.ConfigurableApplicationContext;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.ActionImpl;
import org.alfresco.repo.action.RuntimeActionService;
import org.alfresco.repo.jscript.ClasspathScriptLocation;
import org.alfresco.repo.lock.JobLockService;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.replication.script.ScriptReplicationDefinition;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transfer.TransferServiceImpl2;
import org.alfresco.repo.transfer.TransferTransmitter;
import org.alfresco.repo.transfer.UnitTestInProcessTransmitterImpl;
import org.alfresco.repo.transfer.UnitTestTransferManifestNodeFactory;
import org.alfresco.repo.transfer.manifest.TransferManifestNodeFactory;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.action.ActionStatus;
import org.alfresco.service.cmr.action.ActionTrackingService;
import org.alfresco.service.cmr.action.scheduled.SchedulableAction.IntervalPeriod;
import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedActionService;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.lock.UnableToReleaseLockException;
import org.alfresco.service.cmr.replication.DisabledReplicationJobException;
import org.alfresco.service.cmr.replication.ReplicationDefinition;
import org.alfresco.service.cmr.replication.ReplicationService;
import org.alfresco.service.cmr.replication.ReplicationServiceException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.Path;
import org.alfresco.service.cmr.repository.ScriptLocation;
import org.alfresco.service.cmr.repository.ScriptService;
import org.alfresco.service.cmr.transfer.TransferDefinition;
import org.alfresco.service.cmr.transfer.TransferException;
import org.alfresco.service.cmr.transfer.TransferReceiver;
import org.alfresco.service.cmr.transfer.TransferService2;
import org.alfresco.service.cmr.transfer.TransferTarget;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.alfresco.util.testing.category.LuceneTests;

/**
 * Unit tests for the Replication Service. Handles its own transactions, as in a few cases it needs to run async actions and know how they'll behave
 * 
 * @author Nick Burch
 */
@Category({OwnJVMTestsCategory.class, LuceneTests.class})
public class ReplicationServiceIntegrationTest extends TestCase
{
    private ConfigurableApplicationContext ctx;
    private static final Log log = LogFactory.getLog(ReplicationServiceIntegrationTest.class);

    private ReplicationActionExecutor replicationActionExecutor;
    private ReplicationService replicationService;
    private ReplicationParams replicationParams;
    private TransactionService transactionService;
    private TransferService2 transferService;
    private ContentService contentService;
    private JobLockService jobLockService;
    private ScriptService scriptService;
    private ActionService actionService;
    private NodeService nodeService;
    private LockService lockService;
    private Repository repositoryHelper;
    private ActionTrackingService actionTrackingService;
    private ScheduledPersistedActionService scheduledPersistedActionService;

    private NodeRef replicationRoot;

    private NodeRef destinationFolder;
    private NodeRef folder1;
    private NodeRef folder2;
    private NodeRef folder2a;
    private NodeRef folder2b;
    private NodeRef content1_1;
    private NodeRef content1_2;
    private NodeRef thumbnail1_3; // Thumbnail extends content
    private NodeRef authority1_4; // Authority doesn't
    private NodeRef content2a_1;
    private NodeRef thumbnail2a_2; // Thumbnail extends content
    private NodeRef zone2a_3; // Zone doesn't
    private NodeRef deletedFolder;

    private final String ACTION_NAME = "testName";
    private final String ACTION_NAME2 = "testName2";
    private final String ACTION_NAME3 = "testName3";
    private final QName ACTION_QNAME = QName.createQName(null, ACTION_NAME);
    private final QName ACTION_QNAME2 = QName.createQName(null, ACTION_NAME2);

    private final String TRANSFER_TARGET = "TestTransferTarget";

    @Override
    protected void setUp() throws Exception
    {
        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
        {
            fail("Dangling transaction detected, left by a previous test.");
        }
        ctx = (ConfigurableApplicationContext) ApplicationContextHelper.getApplicationContext();
        replicationActionExecutor = (ReplicationActionExecutor) ctx.getBean("replicationActionExecutor");
        replicationService = (ReplicationService) ctx.getBean("replicationService");
        replicationParams = (ReplicationParams) ctx.getBean("replicationParams");
        transactionService = (TransactionService) ctx.getBean("transactionService");
        transferService = (TransferService2) ctx.getBean("transferService2");
        contentService = (ContentService) ctx.getBean("contentService");
        jobLockService = (JobLockService) ctx.getBean("jobLockService");
        actionService = (ActionService) ctx.getBean("actionService");
        scriptService = (ScriptService) ctx.getBean("scriptService");
        nodeService = (NodeService) ctx.getBean("NodeService");
        lockService = (LockService) ctx.getBean("lockService");
        repositoryHelper = (Repository) ctx.getBean("repositoryHelper");
        actionTrackingService = (ActionTrackingService) ctx.getBean("actionTrackingService");
        scheduledPersistedActionService = (ScheduledPersistedActionService) ctx.getBean("scheduledPersistedActionService");

        // Set the current security context as admin
        AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());

        replicationParams.setEnabled(true);

        UserTransaction txn = transactionService.getUserTransaction();
        txn.begin();

        // Zap any existing replication entries
        replicationRoot = ReplicationDefinitionPersisterImpl.REPLICATION_ACTION_ROOT_NODE_REF;
        for (ChildAssociationRef child : nodeService.getChildAssocs(replicationRoot))
        {
            QName type = nodeService.getType(child.getChildRef());
            if (ReplicationDefinitionPersisterImpl.ACTION_TYPES.contains(type))
            {
                nodeService.deleteNode(child.getChildRef());
            }
        }

        // Create the test folder structure
        destinationFolder = makeNode(repositoryHelper.getCompanyHome(), ContentModel.TYPE_FOLDER, "ReplicationTransferDestination");
        folder1 = makeNode(repositoryHelper.getCompanyHome(), ContentModel.TYPE_FOLDER);
        folder2 = makeNode(repositoryHelper.getCompanyHome(), ContentModel.TYPE_FOLDER);
        folder2a = makeNode(folder2, ContentModel.TYPE_FOLDER);
        folder2b = makeNode(folder2, ContentModel.TYPE_FOLDER);

        content1_1 = makeNode(folder1, ContentModel.TYPE_CONTENT);
        content1_2 = makeNode(folder1, ContentModel.TYPE_CONTENT);
        thumbnail1_3 = makeNode(folder1, ContentModel.TYPE_THUMBNAIL);
        authority1_4 = makeNode(folder1, ContentModel.TYPE_AUTHORITY);
        content2a_1 = makeNode(folder2a, ContentModel.TYPE_CONTENT);
        thumbnail2a_2 = makeNode(folder2a, ContentModel.TYPE_THUMBNAIL);
        zone2a_3 = makeNode(folder2a, ContentModel.TYPE_ZONE);

        deletedFolder = makeNode(repositoryHelper.getCompanyHome(), ContentModel.TYPE_FOLDER);
        nodeService.deleteNode(deletedFolder);

        // Tell the transfer service not to use HTTP
        makeTransferServiceLocal();

        // Finish setup
        txn.commit();
    }

    @Override
    protected void tearDown() throws Exception
    {
        RetryingTransactionCallback<Void> cleanupCallback = new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Zap our test folders
                if (folder1 != null)
                {
                    nodeService.deleteNode(folder1);
                }
                if (folder2 != null)
                {
                    nodeService.deleteNode(folder2);
                }
                // Zap the destination folder, which may well contain entries transfered over which are locked
                if (destinationFolder != null)
                {
                    lockService.unlock(destinationFolder, true);
                    nodeService.deleteNode(destinationFolder);
                }
                return null;
            }
        };
        transactionService.getRetryingTransactionHelper().doInTransaction(cleanupCallback);

        RetryingTransactionCallback<Void> cleanupTargetCallback = new RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                // Zap our test transfer target
                transferService.deleteTransferTarget(TRANSFER_TARGET);
                return null;
            }
        };
        try
        {
            transactionService.getRetryingTransactionHelper().doInTransaction(cleanupTargetCallback);
        }
        catch (TransferException e)
        {
            // Ignore
        }

        if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
        {
            fail("Dangling transaction detected, current test failed to tidy up.");
        }
    }

    public void testCreation() throws Exception
    {
        ReplicationDefinition replicationAction = replicationService.createReplicationDefinition(ACTION_NAME, "Test Definition");
        assertNotNull(replicationAction);
        assertEquals("Test Definition", replicationAction.getDescription());
        assertEquals(ACTION_NAME, replicationAction.getReplicationName());
        assertEquals(ACTION_QNAME, replicationAction.getReplicationQName());

        String id = replicationAction.getId();
        assertNotNull(id);
        assertTrue(id.length() > 0);

        assertNotNull(replicationAction.getPayload());
        assertEquals(0, replicationAction.getPayload().size());

        assertNull(replicationAction.getLocalTransferReport());
        assertNull(replicationAction.getRemoteTransferReport());
    }

    public void testCreateSaveLoad() throws Exception
    {
        ReplicationDefinition replicationAction = replicationService.createReplicationDefinition(ACTION_NAME, "Test Definition");
        String initialId = replicationAction.getId();
        replicationAction.getPayload().add(
                new NodeRef("workspace://SpacesStore/Testing"));
        replicationAction.getPayload().add(
                new NodeRef("workspace://SpacesStore/Testing2"));
        assertEquals(2, replicationAction.getPayload().size());

        replicationService.saveReplicationDefinition(replicationAction);

        // Load it again, should have the same details still
        ReplicationDefinition retrieved = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertNotNull(retrieved);
        assertEquals(initialId, retrieved.getId());
        assertEquals(ACTION_NAME, retrieved.getReplicationName());
        assertEquals(ACTION_QNAME, retrieved.getReplicationQName());
        assertEquals("Test Definition", retrieved.getDescription());
        assertEquals(2, retrieved.getPayload().size());

        // Load a 2nd copy, won't be any changes
        ReplicationDefinition second = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertNotNull(second);
        assertEquals(initialId, second.getId());
        assertEquals(ACTION_NAME, second.getReplicationName());
        assertEquals(ACTION_QNAME, second.getReplicationQName());
        assertEquals("Test Definition", second.getDescription());
        assertEquals(2, second.getPayload().size());
    }

    public void testLoadList() throws Exception
    {
        assertEquals(0, replicationService.loadReplicationDefinitions().size());

        // Create and store
        ReplicationDefinition rd1 = replicationService.createReplicationDefinition(ACTION_NAME, "Test 1");
        ReplicationDefinition rd2 = replicationService.createReplicationDefinition(ACTION_NAME2, "Test 2");

        assertEquals(0, replicationService.loadReplicationDefinitions().size());

        replicationService.saveReplicationDefinition(rd1);

        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(ACTION_NAME, replicationService.loadReplicationDefinitions().get(0).getReplicationName());

        replicationService.saveReplicationDefinition(rd2);
        assertEquals(2, replicationService.loadReplicationDefinitions().size());
    }

    public void testLoadByTarget() throws Exception
    {
        assertEquals(0, replicationService.loadReplicationDefinitions().size());
        assertEquals(0, replicationService.loadReplicationDefinitions("TestTarget").size());
        assertEquals(0, replicationService.loadReplicationDefinitions("TestTarget2").size());

        // Store some
        ReplicationDefinition rdTT = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rdTT.setTargetName("TestTarget");
        replicationService.saveReplicationDefinition(rdTT);

        // Check it shows up correctly
        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(1, replicationService.loadReplicationDefinitions("TestTarget").size());
        assertEquals(0, replicationService.loadReplicationDefinitions("TestTarget2").size());
    }

    /**
     * Ensures that deletion works correctly
     */
    public void testDeletion() throws Exception
    {
        // Delete does nothing if not persisted
        assertEquals(0, replicationService.loadReplicationDefinitions().size());
        ReplicationDefinition rd1 = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        assertEquals(0, replicationService.loadReplicationDefinitions().size());

        replicationService.deleteReplicationDefinition(rd1);
        assertEquals(0, replicationService.loadReplicationDefinitions().size());

        // Create and save two
        ReplicationDefinition rd2 = replicationService.createReplicationDefinition(ACTION_NAME2, "Test2");
        replicationService.saveReplicationDefinition(rd1);
        replicationService.saveReplicationDefinition(rd2);
        assertEquals(2, replicationService.loadReplicationDefinitions().size());

        // Delete one - the correct one goes!
        replicationService.deleteReplicationDefinition(rd2);
        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(ACTION_NAME, replicationService.loadReplicationDefinitions().get(0).getReplicationName());
        assertNotNull(replicationService.loadReplicationDefinition(ACTION_NAME));
        assertNull(replicationService.loadReplicationDefinition(ACTION_NAME2));

        // Re-delete already deleted, no change
        replicationService.deleteReplicationDefinition(rd2);
        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(ACTION_NAME, replicationService.loadReplicationDefinitions().get(0).getReplicationName());
        assertNotNull(replicationService.loadReplicationDefinition(ACTION_NAME));
        assertNull(replicationService.loadReplicationDefinition(ACTION_NAME2));

        // Delete the 2nd
        replicationService.deleteReplicationDefinition(rd1);
        assertEquals(0, replicationService.loadReplicationDefinitions().size());
        assertNull(replicationService.loadReplicationDefinition(ACTION_NAME));
        assertNull(replicationService.loadReplicationDefinition(ACTION_NAME2));

        // Can add back in again after being deleted
        replicationService.saveReplicationDefinition(rd1);
        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(ACTION_NAME, replicationService.loadReplicationDefinitions().get(0).getReplicationName());
    }

    /**
     * Ensures that we can create, save, edit, save load, edit, save, load etc, all without problems, and without creating duplicates
     */
    public void testEditing() throws Exception
    {
        ReplicationDefinition rdTT = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rdTT.setTargetName("TestTarget");
        replicationService.saveReplicationDefinition(rdTT);

        // Load, and check it hasn't changed
        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(ACTION_NAME, rdTT.getReplicationName());
        assertEquals("Test", rdTT.getDescription());
        assertEquals("TestTarget", rdTT.getTargetName());
        assertEquals(true, rdTT.isEnabled());
        assertEquals(0, rdTT.getPayload().size());

        // Save and re-load without changes
        replicationService.saveReplicationDefinition(rdTT);
        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(ACTION_NAME, rdTT.getReplicationName());
        assertEquals("Test", rdTT.getDescription());
        assertEquals("TestTarget", rdTT.getTargetName());
        assertEquals(true, rdTT.isEnabled());
        assertEquals(0, rdTT.getPayload().size());

        // Make some small changes
        rdTT.setDescription("Test Description");
        rdTT.getPayload().add(folder2a);
        rdTT.setEnabled(false);

        // Check we see them on save/load
        replicationService.saveReplicationDefinition(rdTT);
        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(ACTION_NAME, rdTT.getReplicationName());
        assertEquals("Test Description", rdTT.getDescription());
        assertEquals("TestTarget", rdTT.getTargetName());
        assertEquals(false, rdTT.isEnabled());
        assertEquals(1, rdTT.getPayload().size());
        assertEquals(folder2a, rdTT.getPayload().get(0));

        // And some more changes
        rdTT.setDescription("Another One");
        rdTT.getPayload().clear();
        rdTT.getPayload().add(folder1);
        rdTT.getPayload().add(folder2b);
        assertEquals(2, rdTT.getPayload().size());

        // Ensure these also come with save/load
        replicationService.saveReplicationDefinition(rdTT);
        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(ACTION_NAME, rdTT.getReplicationName());
        assertEquals("Another One", rdTT.getDescription());
        assertEquals("TestTarget", rdTT.getTargetName());
        assertEquals(false, rdTT.isEnabled());
        assertEquals(2, rdTT.getPayload().size());
        assertEquals(folder1, rdTT.getPayload().get(0));
        assertEquals(folder2b, rdTT.getPayload().get(1));

        // And more payload changes
        rdTT.getPayload().clear();
        rdTT.getPayload().add(content1_1);
        assertEquals(1, rdTT.getPayload().size());
        rdTT.setEnabled(true);

        replicationService.saveReplicationDefinition(rdTT);
        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(ACTION_NAME, rdTT.getReplicationName());
        assertEquals("Another One", rdTT.getDescription());
        assertEquals("TestTarget", rdTT.getTargetName());
        assertEquals(true, rdTT.isEnabled());
        assertEquals(1, rdTT.getPayload().size());
        assertEquals(content1_1, rdTT.getPayload().get(0));
    }

    /**
     * Tests that we can rename definitions
     */
    public void testRenaming() throws Exception
    {
        // Create one instance
        ReplicationDefinition rdTT = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rdTT.setTargetName("TestTarget");
        replicationService.saveReplicationDefinition(rdTT);
        assertEquals(1, replicationService.loadReplicationDefinitions().size());

        // Rename it
        replicationService.renameReplicationDefinition(ACTION_NAME, ACTION_NAME2);

        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(null, replicationService.loadReplicationDefinition(ACTION_NAME));

        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME2);
        assertNotNull(rdTT);
        assertEquals(ACTION_NAME2, rdTT.getReplicationName());
        assertEquals(ACTION_QNAME2, rdTT.getReplicationQName());

        // If the source name doesn't exist, does nothing
        replicationService.renameReplicationDefinition(ACTION_NAME, ACTION_NAME2);

        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        assertEquals(null, replicationService.loadReplicationDefinition(ACTION_NAME));

        rdTT = replicationService.loadReplicationDefinition(ACTION_NAME2);
        assertNotNull(rdTT);
        assertEquals(ACTION_NAME2, rdTT.getReplicationName());
        assertEquals(ACTION_QNAME2, rdTT.getReplicationQName());

        // Renaming to a duplicate name breaks
        rdTT = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rdTT.setTargetName("TestTarget");
        replicationService.saveReplicationDefinition(rdTT);
        assertEquals(2, replicationService.loadReplicationDefinitions().size());

        try
        {
            replicationService.renameReplicationDefinition(ACTION_NAME, ACTION_NAME2);
            fail("Shouldn't be able to rename onto a duplicate name");
        }
        catch (ReplicationServiceException e)
        {}
    }

    /**
     * Test that the action service can find the executor for us, and that it has everything it needs
     */
    public void testBasicExecution() throws Exception
    {
        // We need the test transfer target for this test
        makeTransferTarget();

        // Ensure the destination is empty
        // (don't want to get confused with older runs)
        assertEquals(0, nodeService.getChildAssocs(destinationFolder).size());

        // First one with no target, which isn't allowed
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        UserTransaction txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
            fail("Shouldn't be permitted with no Target defined");
        }
        catch (ReplicationServiceException e)
        {}
        txn.rollback();

        // Now no payload, also not allowed
        rd.setTargetName(TRANSFER_TARGET);
        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
            fail("Shouldn't be permitted with no payload defined");
        }
        catch (ReplicationServiceException e)
        {}
        txn.rollback();

        // Invalid Transfer Target, not allowed
        rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName("I am an invalid target that isn't there");
        rd.getPayload().add(folder1);
        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
            fail("Shouldn't be permitted with an invalid transfer target");
        }
        catch (ReplicationServiceException e)
        {}
        txn.rollback();

        // Can't send Folder2a if Folder2 isn't there, as it
        // won't have anywhere to put it
        rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName(TRANSFER_TARGET);
        rd.getPayload().add(folder2a);
        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
            fail("Shouldn't be able to send Folder2a when Folder2 is missing!");
        }
        catch (ReplicationServiceException e)
        {}
        txn.rollback();

        // Next a proper one with a transient definition,
        // and a sensible set of folders
        rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName(TRANSFER_TARGET);
        rd.getPayload().add(folder1);
        // A deleted folder is fine, will be skipped
        rd.getPayload().add(deletedFolder);

        // Will execute without error
        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
        }
        catch (ReplicationServiceException e)
        {
            // This shouldn't happen normally! Something is wrong!
            // Tidy up before we throw the exception
            txn.rollback();
            throw e;
        }
        txn.commit();

        // Now with one that's in the repo
        ReplicationDefinition rd2 = replicationService.createReplicationDefinition(ACTION_NAME2, "Test");
        rd2.setTargetName(TRANSFER_TARGET);
        rd2.getPayload().add(
                folder2);
        replicationService.saveReplicationDefinition(rd2);
        rd2 = replicationService.loadReplicationDefinition(ACTION_NAME2);

        // Again no errors
        txn = transactionService.getUserTransaction();
        txn.begin();
        actionService.executeAction(rd2, replicationRoot);
        txn.commit();

        // Now disabled, not allowed
        assertEquals(true, rd.isEnabled());
        rd.setEnabled(false);
        assertEquals(false, rd.isEnabled());
        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
            fail("Shouldn't be permitted when disabled");
        }
        catch (ReplicationServiceException e)
        {
            // check if throwed exception is of expected type
            assertTrue(e instanceof DisabledReplicationJobException);
            assertTrue(actionService instanceof RuntimeActionService);
            if (actionService instanceof RuntimeActionService)
            {
                RuntimeActionService runtimeActionService = (RuntimeActionService) actionService;
                // check if throwed exception is considered handled
                assertTrue(runtimeActionService.onLogException(rd, log, e, e.getMessage()));
            }

        }
        txn.rollback();
        rd.setEnabled(true);

        // Schedule it for 0.5 seconds into the future
        // Ensure that it is run to completion
        txn = transactionService.getUserTransaction();
        txn.begin();

        ((ActionImpl) rd2).setExecutionStatus(ActionStatus.New);

        replicationService.enableScheduling(rd2);
        rd2.setScheduleStart(new Date(System.currentTimeMillis() + 500));
        replicationService.saveReplicationDefinition(rd2);

        txn.commit();

        // Wait for it to run
        Thread.sleep(2000);
        for (int i = 0; i < 100; i++)
        {
            txn = transactionService.getUserTransaction();
            txn.begin();
            rd2 = replicationService.loadReplicationDefinition(ACTION_NAME2);
            txn.commit();

            if (rd2.getExecutionStatus().equals(ActionStatus.New) ||
                    rd2.getExecutionStatus().equals(ActionStatus.Pending) ||
                    rd2.getExecutionStatus().equals(ActionStatus.Running))
            {
                Thread.sleep(50);
            }
        }

        // Check it worked
        assertEquals(ActionStatus.Completed, rd2.getExecutionStatus());
    }

    /**
     * Check that the locking works. Take a 10 second lock on the job, then execute. Ensure that we really wait a little over 10 seconds.
     */
    public void testReplicationExecutionLocking() throws Exception
    {
        // We need the test transfer target for this test
        makeTransferTarget();

        // Create a task
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName(TRANSFER_TARGET);
        rd.getPayload().add(folder1);
        rd.getPayload().add(folder2);

        // Get the lock, and run
        long start = System.currentTimeMillis();
        String token = jobLockService.getLock(
                rd.getReplicationQName(),
                10 * 1000,
                1,
                1);

        UserTransaction txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
        }
        catch (ReplicationServiceException e)
        {
            // This shouldn't happen normally! Something is wrong!
            // Tidy up before we throw the exception
            txn.rollback();
            throw e;
        }
        txn.commit();
        long end = System.currentTimeMillis();

        assertTrue(
                "Should wait for the lock, but didn't (waited " +
                        ((end - start) / 1000.0) + " seconds, not 10)",
                end - start > 10000);
    }

    /**
     * Check that cancelling works. Does this by taking a lock on the job, cancelling, releasing and seeing it abort.
     * 
     * Tests that when we ask for a replication task to be cancelled, that it starts, cancels, and the status is correctly recorded for it.
     */
    public void testReplicationExecutionCancelling() throws Exception
    {
        // We need the test transfer target for this test
        makeTransferTarget();

        // Create a task
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName(TRANSFER_TARGET);
        rd.getPayload().add(folder1);
        rd.getPayload().add(folder2);

        // Get the lock for 2 seconds
        String token = jobLockService.getLock(
                rd.getReplicationQName(),
                2 * 1000,
                1,
                1);

        // Request it be run async
        UserTransaction txn = transactionService.getUserTransaction();
        txn.begin();
        actionService.executeAction(rd, replicationRoot, false, true);
        assertEquals(ActionStatus.Pending, rd.getExecutionStatus());

        assertEquals(false, actionTrackingService.isCancellationRequested(rd));
        actionTrackingService.requestActionCancellation(rd);
        assertEquals(true, actionTrackingService.isCancellationRequested(rd));

        txn.commit();

        // Let it get going, will be waiting for the lock
        // having registered with the action tracking service
        for (int i = 0; i < 100; i++)
        {
            // Keep asking for it to be cancelled ASAP
            actionTrackingService.requestActionCancellation(rd);

            if (rd.getExecutionStatus().equals(ActionStatus.Running))
            {
                // Good, has started up
                // Stop waiting and do the cancel
                break;
            }
            else
            {
                // Still pending, wait a bit more
                Thread.sleep(10);
            }
        }

        // Ensure it started, and should shortly stop
        assertEquals(ActionStatus.Running, rd.getExecutionStatus());
        assertEquals(true, actionTrackingService.isCancellationRequested(rd));

        // Release our lock, should allow the replication task
        // to get going and spot the cancel
        jobLockService.releaseLock(token, rd.getReplicationQName());

        // Let the main replication task run to cancelled/completed
        // This can take quite some time though...
        for (int i = 0; i < 10; i++)
        {
            if (rd.getExecutionStatus() == ActionStatus.Running)
            {
                Thread.sleep(1000);
            }
            else
            {
                // It has finished running, check it
                break;
            }
        }

        // Ensure it was cancelled
        assertEquals(null, rd.getExecutionFailureMessage());
        assertNotNull(rd.getLocalTransferReport());
        assertNotNull(rd.getRemoteTransferReport());
        assertEquals(ActionStatus.Cancelled, rd.getExecutionStatus());
    }

    /**
     * Test that when we execute a replication task, the right stuff ends up being moved for us
     */
    public void testExecutionResult() throws Exception
    {
        UserTransaction txn = transactionService.getUserTransaction();
        txn.begin();

        // Destination is empty
        assertEquals(0, nodeService.getChildAssocs(destinationFolder).size());

        // We need the test transfer target for this test
        makeTransferTarget();

        // Put in Folder 2, so we can send Folder 2a
        String folder2Name = (String) nodeService.getProperties(folder2).get(ContentModel.PROP_NAME);
        NodeRef folderT2 = makeNode(destinationFolder, ContentModel.TYPE_FOLDER, folder2Name);
        txn.commit();

        // Run a transfer
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName(TRANSFER_TARGET);
        rd.getPayload().add(folder1);
        rd.getPayload().add(folder2a);

        assertEquals(null, rd.getLocalTransferReport());
        assertEquals(null, rd.getRemoteTransferReport());

        txn = transactionService.getUserTransaction();
        txn.begin();
        try
        {
            actionService.executeAction(rd, replicationRoot);
        }
        catch (ReplicationServiceException e)
        {
            // This shouldn't happen normally! Something is wrong!
            // Tidy up before we throw the exception
            txn.rollback();
            throw e;
        }
        txn.commit();

        // Correct things have turned up
        assertEquals(2, nodeService.getChildAssocs(destinationFolder).size());
        NodeRef c1 = nodeService.getChildAssocs(destinationFolder).get(0).getChildRef();
        NodeRef c2 = nodeService.getChildAssocs(destinationFolder).get(1).getChildRef();

        // The destination should have folder 1 (transfered)
        // and folder 2 (created). folder 2 will have
        // folder 2a (transfered) but not 2b
        NodeRef folderT1 = null;
        boolean foundT1 = false;
        boolean foundT2 = false;
        if (nodeService.getProperty(folder1, ContentModel.PROP_NAME).equals(
                nodeService.getProperty(c1, ContentModel.PROP_NAME)))
        {
            folderT1 = c1;
            foundT1 = true;
        }
        if (nodeService.getProperty(folder1, ContentModel.PROP_NAME).equals(
                nodeService.getProperty(c2, ContentModel.PROP_NAME)))
        {
            folderT1 = c2;
            foundT1 = true;
        }
        if (c1.equals(folderT2) || c2.equals(folderT2))
        {
            foundT2 = true;
        }

        if (!foundT1)
        {
            fail("Folder 1 not found in the destination");
        }
        if (!foundT2)
        {
            fail("Folder 2 not found in the destination");
        }

        // Folder 1 has 2*content + thumbnail
        assertEquals(3, nodeService.getChildAssocs(folderT1).size());
        // Won't have the authority, as that gets skipped
        for (ChildAssociationRef r : nodeService.getChildAssocs(folderT1))
        {
            if (nodeService.getType(r.getChildRef()).equals(ContentModel.TYPE_AUTHORITY))
            {
                fail("Found authority as " + r.getChildRef() + " but it shouldn't be transfered!");
            }
        }

        // Folder 2 has 2a but not 2b, since only
        // 2a was transfered
        assertEquals(1, nodeService.getChildAssocs(folderT2).size());
        NodeRef folderT2a = nodeService.getChildAssocs(folderT2).get(0).getChildRef();
        assertEquals(
                nodeService.getProperty(folder2a, ContentModel.PROP_NAME),
                nodeService.getProperty(folderT2a, ContentModel.PROP_NAME));
        // Won't have Folder 2b, as it wasn't on the payload
        for (ChildAssociationRef r : nodeService.getChildAssocs(folderT2))
        {
            assertNotSame(
                    nodeService.getProperty(folder2b, ContentModel.PROP_NAME),
                    nodeService.getProperty(r.getChildRef(), ContentModel.PROP_NAME));
        }

        // Folder 2a has content + thumbnail
        assertEquals(2, nodeService.getChildAssocs(folderT2a).size());
        // Won't have the zone, as that gets skipped
        for (ChildAssociationRef r : nodeService.getChildAssocs(folderT2a))
        {
            if (nodeService.getType(r.getChildRef()).equals(ContentModel.TYPE_ZONE))
            {
                fail("Found zone as " + r.getChildRef() + " but it shouldn't be transfered!");
            }
        }

        // Check we got transfer reports, and they look sensible
        NodeRef localReport = rd.getLocalTransferReport();
        assertNotNull(localReport);
        NodeRef remoteReport = rd.getRemoteTransferReport();
        assertNotNull(remoteReport);

        txn = transactionService.getUserTransaction();
        txn.begin();

        ContentReader localReader = contentService.getReader(localReport, ContentModel.PROP_CONTENT);
        String localReportContent = localReader.getContentString();

        assertTrue("XML not found in:\n" + localReportContent, localReportContent.contains("<?xml"));
        assertTrue("Report XML not found in:\n" + localReportContent, localReportContent.contains("<report:transferReport"));

        ContentReader remoteReader = contentService.getReader(remoteReport, ContentModel.PROP_CONTENT);
        String remoteReportContent = remoteReader.getContentString();

        assertTrue("XML not found in:\n" + remoteReportContent, remoteReportContent.contains("<?xml"));
        assertTrue("Report Status not found in:\n" + remoteReportContent, remoteReportContent.contains("state=\"COMPLETE\""));

        txn.commit();
    }

    /**
     * Test that we turn a list of payload node starting points into the correct set of nodes to pass to the transfer service.
     */
    public void testReplicationPayloadExpansion() throws Exception
    {
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        Set<NodeRef> expanded;

        // Empty folder -> just itself
        rd.getPayload().clear();
        rd.getPayload().add(folder2b);
        expanded = replicationActionExecutor.expandPayload(rd);
        assertEquals(1, expanded.size());
        assertTrue(expanded.contains(folder2b));

        // Folder with content and thumbnails - just content + thumbnail + folder
        rd.getPayload().clear();
        rd.getPayload().add(folder1);
        expanded = replicationActionExecutor.expandPayload(rd);
        assertEquals(4, expanded.size());
        assertTrue(expanded.contains(folder1));
        assertTrue(expanded.contains(content1_1));
        assertTrue(expanded.contains(content1_2));
        assertTrue(expanded.contains(thumbnail1_3));
        assertFalse(expanded.contains(authority1_4)); // Wrong type, won't be there

        // Folder with folders - descends properly
        rd.getPayload().clear();
        rd.getPayload().add(folder2);
        expanded = replicationActionExecutor.expandPayload(rd);
        assertEquals(5, expanded.size());
        assertTrue(expanded.contains(folder2));
        assertTrue(expanded.contains(folder2a));
        assertTrue(expanded.contains(content2a_1));
        assertTrue(expanded.contains(thumbnail2a_2));
        assertFalse(expanded.contains(zone2a_3)); // Wrong type, won't be there
        assertTrue(expanded.contains(folder2b));

        // Multiple things - gets each in turn
        rd.getPayload().clear();
        rd.getPayload().add(folder1);
        rd.getPayload().add(folder2);
        expanded = replicationActionExecutor.expandPayload(rd);
        assertEquals(9, expanded.size());
        assertTrue(expanded.contains(folder1));
        assertTrue(expanded.contains(content1_1));
        assertTrue(expanded.contains(content1_2));
        assertTrue(expanded.contains(thumbnail1_3));
        assertTrue(expanded.contains(folder2));
        assertTrue(expanded.contains(folder2a));
        assertTrue(expanded.contains(content2a_1));
        assertTrue(expanded.contains(thumbnail2a_2));
        assertTrue(expanded.contains(folder2b));

        // TODO Test how options like permissions and renditions
        // affects what gets sent back
    }

    /**
     * Test that we turn a replication definition correctly into a transfer definition
     */
    public void testTransferDefinitionBuilding() throws Exception
    {
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");

        Set<NodeRef> nodes = new HashSet<NodeRef>();
        nodes.add(folder1);
        nodes.add(content1_1);

        TransferDefinition td = replicationActionExecutor.buildTransferDefinition(rd, nodes);
        assertEquals(true, td.isSync());
        assertEquals(replicationParams.getTransferReadOnly(), td.isReadOnly());
        assertEquals(2, td.getNodes().size());
        assertEquals(true, td.getNodes().contains(folder1));
        assertEquals(true, td.getNodes().contains(content1_1));
    }

    private abstract class DoInTransaction implements RetryingTransactionCallback<Void>
    {
        protected final ReplicationDefinition replicationDefinition;

        private DoInTransaction(ReplicationDefinition rd)
        {
            this.replicationDefinition = rd;
        }
    }

    /**
     * Test that the schedule related parts work properly
     */
    public void testScheduling() throws Exception
    {
        // A new definition doesn't have scheduling
        ReplicationDefinition rd = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
        rd.setTargetName("Target");
        assertFalse(rd.isSchedulingEnabled());

        // Disable does nothing
        replicationService.disableScheduling(rd);
        assertFalse(rd.isSchedulingEnabled());

        // Enable it
        transactionService.getRetryingTransactionHelper().doInTransaction(
                new DoInTransaction(rd) {
                    public Void execute() throws Throwable
                    {
                        replicationService.saveReplicationDefinition(replicationDefinition);
                        replicationService.enableScheduling(replicationDefinition);
                        assertTrue(replicationDefinition.isSchedulingEnabled());
                        return null;
                    }
                }, false, true);

        assertTrue(rd.isSchedulingEnabled());

        // Double enabling does nothing
        replicationService.enableScheduling(rd);
        assertTrue(rd.isSchedulingEnabled());

        // Change it
        assertNull(rd.getScheduleStart());
        assertNull(rd.getScheduleIntervalCount());
        assertNull(rd.getScheduleIntervalPeriod());

        rd.setScheduleStart(new Date(1));

        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(null, rd.getScheduleIntervalCount());
        assertEquals(null, rd.getScheduleIntervalPeriod());

        // Won't show up until saved
        ReplicationDefinition rd2 = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(false, rd2.isSchedulingEnabled());
        assertEquals(null, rd2.getScheduleStart());
        assertEquals(null, rd2.getScheduleIntervalCount());
        assertEquals(null, rd2.getScheduleIntervalPeriod());

        // Save and check
        assertEquals(true, rd.isSchedulingEnabled());

        transactionService.getRetryingTransactionHelper().doInTransaction(
                new DoInTransaction(rd) {
                    public Void execute() throws Throwable
                    {
                        replicationService.saveReplicationDefinition(replicationDefinition);
                        return null;
                    }
                }, false, true);

        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(null, rd.getScheduleIntervalCount());
        assertEquals(null, rd.getScheduleIntervalPeriod());

        rd = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(null, rd.getScheduleIntervalCount());
        assertEquals(null, rd.getScheduleIntervalPeriod());

        // Change, save, check
        rd.setScheduleIntervalCount(2);
        rd.setScheduleIntervalPeriod(IntervalPeriod.Hour);

        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(2, rd.getScheduleIntervalCount().intValue());
        assertEquals(IntervalPeriod.Hour, rd.getScheduleIntervalPeriod());

        transactionService.getRetryingTransactionHelper().doInTransaction(
                new DoInTransaction(rd) {
                    public Void execute() throws Throwable
                    {
                        replicationService.saveReplicationDefinition(replicationDefinition);
                        return null;
                    }
                }, false, true);

        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(2, rd.getScheduleIntervalCount().intValue());
        assertEquals(IntervalPeriod.Hour, rd.getScheduleIntervalPeriod());

        rd = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(2, rd.getScheduleIntervalCount().intValue());
        assertEquals(IntervalPeriod.Hour, rd.getScheduleIntervalPeriod());

        // Re-load and enable is fine
        rd2 = replicationService.loadReplicationDefinition(ACTION_NAME);
        assertEquals(true, rd2.isSchedulingEnabled());
        replicationService.enableScheduling(rd2);
        assertEquals(true, rd2.isSchedulingEnabled());

        // Check on the listing methods
        assertEquals(1, replicationService.loadReplicationDefinitions().size());
        rd = replicationService.loadReplicationDefinitions().get(0);
        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(2, rd.getScheduleIntervalCount().intValue());
        assertEquals(IntervalPeriod.Hour, rd.getScheduleIntervalPeriod());

        assertEquals(1, replicationService.loadReplicationDefinitions("Target").size());
        rd = replicationService.loadReplicationDefinitions("Target").get(0);
        assertEquals(true, rd.isSchedulingEnabled());
        assertEquals(1, rd.getScheduleStart().getTime());
        assertEquals(2, rd.getScheduleIntervalCount().intValue());
        assertEquals(IntervalPeriod.Hour, rd.getScheduleIntervalPeriod());

        // Disable it
        transactionService.getRetryingTransactionHelper().doInTransaction(new DoInTransaction(rd) {
            public Void execute() throws Throwable
            {
                replicationService.disableScheduling(replicationDefinition);
                return null;
            }
        });
        assertEquals(false, rd.isSchedulingEnabled());

        // Check listings again
        rd = replicationService.loadReplicationDefinitions().get(0);
        assertEquals(false, rd.isSchedulingEnabled());

        rd = replicationService.loadReplicationDefinitions("Target").get(0);
        assertEquals(false, rd.isSchedulingEnabled());

        // Enable it, and check the scheduled service
        final int count = scheduledPersistedActionService.listSchedules().size();
        transactionService.getRetryingTransactionHelper().doInTransaction(
                new DoInTransaction(rd) {
                    public Void execute() throws Throwable
                    {
                        replicationService.enableScheduling(replicationDefinition);
                        replicationService.saveReplicationDefinition(replicationDefinition);
                        assertEquals(count + 1, scheduledPersistedActionService.listSchedules().size());
                        return null;
                    }
                }, false, true);

        // Delete it, and check the scheduled service
        transactionService.getRetryingTransactionHelper().doInTransaction(
                new RetryingTransactionCallback<Void>() {
                    public Void execute() throws Throwable
                    {
                        ReplicationDefinition replicationDefinition;
                        replicationDefinition = replicationService.loadReplicationDefinition(ACTION_NAME);
                        replicationService.deleteReplicationDefinition(replicationDefinition);
                        assertEquals(count, scheduledPersistedActionService.listSchedules().size());
                        return null;
                    }
                }, false, true);
        assertEquals(count, scheduledPersistedActionService.listSchedules().size());

        // Ask for it to run scheduled
        // Should fire up and then fail due to missing definitions
        transactionService.getRetryingTransactionHelper().doInTransaction(
                new RetryingTransactionCallback<Void>() {
                    public Void execute() throws Throwable
                    {
                        ReplicationDefinition replicationDefinition;
                        replicationDefinition = replicationService.createReplicationDefinition(ACTION_NAME, "Test");
                        replicationService.enableScheduling(replicationDefinition);
                        replicationDefinition.setScheduleStart(new Date(System.currentTimeMillis() + 50));
                        replicationService.saveReplicationDefinition(replicationDefinition);
                        assertEquals(ActionStatus.New, replicationDefinition.getExecutionStatus());
                        return null;
                    }
                }, false, true);

        // Let it fire up, wait up to 1.5 seconds
        for (int i = 0; i < 150; i++)
        {
            rd = replicationService.loadReplicationDefinition(ACTION_NAME);
            if (rd.getExecutionStatus().equals(ActionStatus.Failed))
                break;
            if (rd.getExecutionStatus().equals(ActionStatus.Completed))
                break;
            Thread.sleep(10);
        }

        // Should have failed, as missing target + payload
        assertEquals(ActionStatus.Failed, rd.getExecutionStatus());
    }

    public void testJavascriptAPI() throws Exception
    {
        ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry");

        // Setup some replication tasks
        ReplicationDefinition empty = replicationService.createReplicationDefinition(ACTION_NAME, "Empty");

        ReplicationDefinition persisted = replicationService.createReplicationDefinition(ACTION_NAME2, "Persisted");
        persisted.setTargetName(TRANSFER_TARGET);
        persisted.getPayload().add(
                new NodeRef("workspace://SpacesStore/Testing"));
        persisted.getPayload().add(
                new NodeRef("workspace://SpacesStore/Testing2"));
        replicationService.saveReplicationDefinition(persisted);

        ReplicationDefinition persisted2 = replicationService.createReplicationDefinition(ACTION_NAME3, "Persisted2");
        persisted2.setTargetName("AnotherTarget");
        replicationService.saveReplicationDefinition(persisted2);

        // Call the test
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("Empty", new ScriptReplicationDefinition(serviceRegistry, replicationService, null, empty));
        model.put("EmptyName", ACTION_NAME);
        model.put("Persisted", new ScriptReplicationDefinition(serviceRegistry, replicationService, null, persisted));
        model.put("PersistedName", ACTION_NAME2);
        model.put("PersistedNodeRef", persisted.getNodeRef().toString());
        model.put("PersistedTarget", persisted.getTargetName());
        model.put("Persisted2", new ScriptReplicationDefinition(serviceRegistry, replicationService, null, persisted2));
        model.put("Persisted2Name", ACTION_NAME3);
        model.put("Persisted2NodeRef", persisted2.getNodeRef().toString());
        model.put("Persisted2Target", persisted2.getTargetName());

        ScriptLocation location = new ClasspathScriptLocation("org/alfresco/repo/replication/script/test_replicationService.js");
        this.scriptService.executeScript(location, model);
    }

    // =============================================

    private NodeRef makeNode(NodeRef parent, QName nodeType)
    {
        String uuid = GUID.generate();
        return makeNode(parent, nodeType, uuid);
    }

    private NodeRef makeNode(NodeRef parent, QName nodeType, String name)
    {
        Map<QName, Serializable> props = new HashMap<QName, Serializable>();

        QName newName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, name);
        NodeRef existing = nodeService.getChildByName(parent, ContentModel.ASSOC_CONTAINS, name);
        if (existing != null)
        {
            System.err.println("Zapped existing node " + existing + " for name " + name);

            try
            {
                lockService.unlock(existing, true);
            }
            catch (UnableToReleaseLockException e)
            {}
            nodeService.deleteNode(existing);
        }

        props.put(ContentModel.PROP_NAME, name);
        ChildAssociationRef assoc = nodeService.createNode(parent, ContentModel.ASSOC_CONTAINS, newName, nodeType, props);
        return assoc.getChildRef();
    }

    private void makeTransferTarget()
    {
        String name = TRANSFER_TARGET;
        String title = "title";
        String description = "description";
        String endpointProtocol = "http";
        String endpointHost = "localhost";
        int endpointPort = 8080;
        String endpointPath = "rhubarb";
        String username = "admin";
        char[] password = "password".toCharArray();

        TransferTarget ret = transferService.createAndSaveTransferTarget(name, title, description, endpointProtocol, endpointHost, endpointPort, endpointPath, username, password);
        assertNotNull("Transfer Target not correctly built", ret);
    }

    private void makeTransferServiceLocal()
    {
        TransferReceiver receiver = (TransferReceiver) ctx.getBean("transferReceiver");
        TransferManifestNodeFactory transferManifestNodeFactory = (TransferManifestNodeFactory) ctx.getBean("transferManifestNodeFactory");
        TransferServiceImpl2 transferServiceImpl = (TransferServiceImpl2) ctx.getBean("transferService2");
        ContentService contentService = (ContentService) ctx.getBean("contentService");

        TransferTransmitter transmitter = new UnitTestInProcessTransmitterImpl(receiver, contentService, transactionService);
        transferServiceImpl.setTransmitter(transmitter);

        UnitTestTransferManifestNodeFactory testNodeFactory = new UnitTestTransferManifestNodeFactory(transferManifestNodeFactory);
        transferServiceImpl.setTransferManifestNodeFactory(testNodeFactory);

        // Map company_home to the special destination folder
        List<Pair<Path, Path>> pathMap = testNodeFactory.getPathMap();
        pathMap.add(new Pair<Path, Path>(
                nodeService.getPath(repositoryHelper.getCompanyHome()),
                nodeService.getPath(destinationFolder)));
    }
}
